package cn.xw.commons.core.utils.fileServerUtils;

import cn.xw.commons.core.utils.fileServerUtils.domain.entity.FileStream;
import cn.xw.commons.core.utils.fileServerUtils.domain.vo.FileDetailsVO;
import org.csource.common.IniFileReader;
import org.csource.common.MyException;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 文件服务器工具类（基于FastDFS官方提供的“fastdfs-client-java”工具类二次封装）
 * 这个是静态类，可以通过类找到指定的方法执行
 *
 * @author Anhui AntLaddie <a href="https://www.cnblogs.com/antLaddie/">(博客园蚂蚁小哥)</a>
 * @version 1.0
 **/
public class FastDFSTools {

    private static final Logger log = LoggerFactory.getLogger(FastDFSTools.class);

    // 存储配置文件读取的信息
    private static final Properties props = new Properties();
    // 创建存储服务器客户端
    private static final StorageClient storageClient;

    public static final Integer BUFFER_FILE_SIZE = 2048;            // 文件上传缓冲流
    private static final String FILE_NAME = "fileName";             // 上传的文件名称
    private static final String FILE_MIME_TYPE = "fileMimeType";    // 文件的MIME类型
    private static final String FILE_EXTENSION = "fileExtension";   // 文件后缀信息
    private static final String FILE_SIZE = "fileSize";             // 文件大小信息

    // 构建日期格式化类
    protected static final SimpleDateFormat sdfA = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // 对象初始化
    static {
        try {
            //加载FastDFS配置文件
            // ClientGlobal.initByProperties("fastdfs-client.properties");
            InputStream in = IniFileReader.loadFromOsFileSystemOrClasspathAsStream("fastdfs-client.properties");
            if (in != null) {
                props.load(in);
            }
            ClientGlobal.initByProperties(props);
            //创建 TrackerClient跟踪器客户端
            TrackerClient trackerClient = new TrackerClient();
            //通过 TrackerClient跟踪器客户端 获取 TrackerServer跟踪器服务端
            TrackerServer trackerServer = trackerClient.getTrackerServer();
            //通过 TrackerClient跟踪器客户端 获取 StorageServer存储服务器服务端
            StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
            //创建存储服务器客户端
            storageClient = new StorageClient(trackerServer, storeStorage);
        } catch (IOException | MyException e) {
            log.error("fastDFS初始化失败，请检查配置文件是否正确：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /***
     * FastDFS文件上传（批量上传）
     * @param fileStreamList 多个文件信息
     */
    public static List<String> uploadFiles(List<FileStream> fileStreamList) {
        // 初始化上传后返回信息
        List<String> uploadUrls = new ArrayList<>();
        // 数据校验
        if (fileStreamList == null || fileStreamList.isEmpty()) {
            log.warn("当前上传到FastDFS的文件信息为空，无法上传！");
            return uploadUrls;
        }
        // 循环上传文件
        for (FileStream fileStream : fileStreamList) {
            uploadUrls.add(uploadFile(fileStream));
        }
        return uploadUrls;
    }

    /***
     * FastDFS文件上传（单个上传）
     * @param fileStream 文件信息
     */
    public static String uploadFile(FileStream fileStream) {
        // 初始化上传后返回信息
        String uploadUrl = "";
        // 数据校验
        if (fileStream == null) {
            log.warn("当前上传到FastDFS的文件信息为空，无法上传！");
            return uploadUrl;
        }
        try {
            // 获取文件名称和文件流信息
            String fileName = fileStream.getFileName();
            InputStream fileInputStream = fileStream.getFileInputStream();

            // 构建FastDFS上传文件元数据
            NameValuePair[] metaList = new NameValuePair[4];
            metaList[0] = new NameValuePair(FILE_NAME, fileName);
            String mimeType = Files.probeContentType(Paths.get(fileName));
            metaList[1] = new NameValuePair(FILE_MIME_TYPE, mimeType);
            long fileSize = fileInputStream.available();
            metaList[2] = new NameValuePair(FILE_SIZE, String.valueOf(fileSize));
            String[] splitArr = fileName.split("\\.");
            String extension = splitArr[splitArr.length - 1];
            metaList[3] = new NameValuePair(FILE_EXTENSION, extension);

            // 构建FastDFS上传回调实现
            UploadCallback uploadCallback = outputStream -> {
                // 每次缓冲上传大小
                byte[] bytes = new byte[BUFFER_FILE_SIZE];
                int index = 0;
                while ((index = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, index);
                }
                // 刷新写入
                outputStream.flush();
                return 0;
            };

            // 具体的文件上传
            //      参数1：上传到指定组位置（如：group1、group2），可以为空，由FastDFS自动选择
            //      参数2：上传的文件大小
            //      参数3：上传文件数据的回调对象
            //      参数4：文件扩展名，不包含“.”
            //      参数5：文件的元数据信息，自由指定，可以用来标记文件
            String[] strings = storageClient.upload_file(null, fileSize, uploadCallback, extension, metaList);
            // 拼接上传成功后的访问URL资源地址
            uploadUrl = props.getProperty("request.address") + strings[0] + "/" + strings[1];
            // 关闭流对象
            fileInputStream.close();
        } catch (IOException | MyException e) {
            throw new RuntimeException("文件信息上传失败：" + e.getMessage());
        }
        return uploadUrl;
    }

    /***
     * 单个文件下载
     * @param url 具体需要下载的URL资源信息
     * @return 返回文件名称+文件流信息
     */
    public static FileStream singleDownload(String url) {
        // 构建返回对象
        FileStream fileStream = new FileStream();
        try {
            if (url != null && !"".equals(url.trim())) {
                String[] urlGroup = urlAnalysis(url);
                // 获取文件的二进制数组并转换为输入流对象
                byte[] bytes = storageClient.download_file(urlGroup[0], urlGroup[1]);
                InputStream inputStream = new ByteArrayInputStream(bytes);
                // 获取原文件名称信息（其实还有用处就是获取文件类型）
                FileDetailsVO fileDetailsVO = fileDetails(url);
                // 组装信息
                fileStream.setFileName("(" + getRandomString(12, true) + ")" + fileDetailsVO.getFileName());
                fileStream.setFileInputStream(inputStream);
            }
            return fileStream;
        } catch (IOException | MyException e) {
            throw new RuntimeException("文件下载失败：" + e.getMessage());
        }
    }

    /***
     * 批量文件下载
     * @param urlList 体需要下载的URL资源信息
     * @return 返回集合文件名称+文件流信息
     */
    public static FileStream manyDownloads(List<String> urlList) {
        // 构建返回对象
        FileStream restFileStream = new FileStream();
        // 批量获取url流信息
        List<FileStream> fileStreamList = new ArrayList<>();
        if (urlList != null && urlList.size() > 0) {
            for (String url : urlList) {
                fileStreamList.add(singleDownload(url));
            }
        }
        // 多个文件进行压缩处理
        // 创建 ZipOutputStream
        try (
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                // 构建zip压缩流
                ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)
        ) {
            // 遍历之前下载的流信息
            for (FileStream fileStream : fileStreamList) {
                // 创建ZipEntry对象，并设置条目的名称，它用于表示ZIP文件中的一个条目
                // 通过调用zipOutputStream.putNextEntry(zipEntry)将该条目添加到ZipOutputStream中，以便在压缩时将文件内容写入该条目。
                // 注：这里多次的文件名称不可相同
                ZipEntry zipEntry = new ZipEntry(fileStream.getFileName());
                zipOutputStream.putNextEntry(zipEntry);
                // 读取文件内容并写入字节数组输出流
                int len;
                byte[] buffer = new byte[2048];
                while ((len = fileStream.getFileInputStream().read(buffer)) > 0) {
                    zipOutputStream.write(buffer, 0, len);
                }
                // 关闭当前条目
                zipOutputStream.closeEntry();
            }
            // 完成压缩，将字节数组输出流中的数据写入响应的OutputStream
            zipOutputStream.finish();
            // 获取压缩后的字节数组
            byte[] compressedData = byteArrayOutputStream.toByteArray();
            // 将压缩数据写入到 InputStream 流中
            InputStream inputStream = new ByteArrayInputStream(compressedData);
            // 设置返回数据
            restFileStream.setFileInputStream(inputStream);
            restFileStream.setFileName(UUID.randomUUID().toString().replace("-", "") + ".zip");
        } catch (IOException e) {
            throw new RuntimeException("文件压缩失败：" + e.getMessage());
        }
        return restFileStream;
    }

    /***
     * 地址加签(也叫：生成防盗链token)（注：只有FastDFS设置了权限时才可以使用加签的url）
     * @param url 未加签的url(其实url后未拼接token)
     * @return 已加签的url
     */
    public static String addSignatureAddress(String url) {
        try {
            // url提取关键信息
            String[] urlArr = urlAnalysis(url);
            // 获取时间秒信息
            int lts = (int) (System.currentTimeMillis() / 1000);
            // 对url进行加签操作
            String token = ProtoCommon.getToken(urlArr[1], lts, props.getProperty("fastdfs.http_secret_key"));
            // 拼接具体的加签后url
            return url + "?token=" + token + "&ts=" + lts;
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException | MyException e) {
            throw new RuntimeException("url地址加签失败：" + e.getMessage());
        }
    }

    /***
     * 文件详细信息
     * @param url 具体资源定位地址
     */
    public static FileDetailsVO fileDetails(String url) {
        // 初始化返回信息
        FileDetailsVO fileDetailsVO = new FileDetailsVO();
        try {
            // 切取主要信息
            String[] urlArr = urlAnalysis(url);
            // 获取之前存储的元数据
            List<NameValuePair> nameValuePairs = Arrays.asList(storageClient.get_metadata(urlArr[0], urlArr[1]));
            // 获取文件的基本信息
            FileInfo fileInfo = storageClient.query_file_info(urlArr[0], urlArr[1]);
            // 设置返回数据
            nameValuePairs.forEach(e -> {
                fileDetailsVO.getFileMetadata().put(e.getName(), e.getValue());
            });
            fileDetailsVO.setFileName(fileDetailsVO.getFileMetadata().get(FILE_NAME));
            fileDetailsVO.setFileSize(new BigDecimal(fileInfo.getFileSize()));
            fileDetailsVO.setCreateDate(fileInfo.getCreateTimestamp());
            fileDetailsVO.setFileAddr(fileInfo.getSourceIpAddr());
            return fileDetailsVO;
        } catch (IOException | MyException e) {
            throw new RuntimeException("解析资源定位地址失败：" + e);
        }
    }

    /***
     * 根据URL信息删除具体资源信息
     * @param url url资源
     */
    public static Integer deleteUrl(String url) {
        // 删除状态
        int status = -1;
        // 对url进行重要信息提取
        String[] urlArr = urlAnalysis(url);
        // 资源信息删除（返回0删除成功，返回2则代表文件不存在）
        try {
            status = storageClient.delete_file(urlArr[0], urlArr[1]);
        } catch (IOException | MyException e) {
            // 删除失败出现异常则记录当前文件信息，由运维人员删除
            writeFastDFSErrDelLog(urlArr);
        }
        return status;
    }

    /***
     * 根据提供的多个url信息批量删除资源信息
     * @param urls url资源集合
     */
    public static void deleteUrls(List<String> urls) {
        if (urls != null && !urls.isEmpty()) {
            // 循环删除
            for (String url : urls) {
                deleteUrl(url);
            }
        }
    }

    /***
     * URL解析，根据一个FastDFS的地址解析出 组信息和目录信息
     * 注意：文件服务器的配置访问目录必须存在组信息才可以解析到组信息
     * @param url 具体资源定位地址
     * @return 字符数组，其中包含[”组信息“,"目录定位信息"]
     */
    public static String[] urlAnalysis(String url) {
        // 判空
        if (url == null || "".equals(url.trim())) {
            throw new IllegalArgumentException("资源URL不可为空！");
        }
        // 剔除请求URL参数
        int indexOf = url.indexOf("?");
        url = url.substring(0, indexOf == -1 ? url.length() : indexOf);
        // 截取组信息和目录定位信息
        String[] split = url.split("/");
        int length = split.length;
        if (length < 5) {
            throw new IllegalArgumentException("资源URL不合法！");
        }
        // 初始化返回信息并填充数据
        String[] strArr = new String[2];
        strArr[1] = split[length - 4] + "/" + split[length - 3] + "/" + split[length - 2] + "/" + split[length - 1];
        strArr[0] = split[length - 5];
        // 校验数据（校验规则，判断是否存在组信息，若没有则失败）
        if (!strArr[0].matches("^group(\\d+)$")) {
            throw new IllegalArgumentException("资源URL不合法！");
        }
        return strArr;
    }

    /***
     * 生成随机字符或数字 len的最大长度为128位
     * @param len 字符串长度
     * @param isNotNumber 是否包含字符 true包含
     * @return 返回指定位数的随机字符串
     */
    private static String getRandomString(Integer len, Boolean isNotNumber) {
        int segment = Math.min(Math.abs(len / 32) + 1, 4);
        StringBuilder randomStr = new StringBuilder();
        for (int i = 0; i < segment; i++) {
            String uuid = UUID.randomUUID().toString().replace("-", "");
            if (isNotNumber) {
                randomStr.append(uuid);
            } else {
                randomStr.append(new BigInteger(uuid, 16));
            }
        }
        return randomStr.substring(0, Math.abs(segment == 4 ? 128 : len));
    }

    /***
     * 记录FastDFS删除失败的文件信息
     * @param urlArr 资源信息
     */
    private static void writeFastDFSErrDelLog(String[] urlArr) {
        // 获取文件路径信息
        File file = new File(Objects.requireNonNull(FastDFSTools.class.getClassLoader()
                .getResource("")).getFile() + "fastDFS_del.log");
        // 组装写入数据
        String str = "===> " + sdfA.format(new Date()) + " 组信息：" + urlArr[0] + " 资源路径信息：" + urlArr[1];

        try (
                // 提前构建写入文本流信息
                BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true))
        ) {
            // 不存在则创建文件
            if (!file.exists()) {
                boolean flag = file.createNewFile();
            }
            bufferedWriter.write(str);
            bufferedWriter.newLine();
            bufferedWriter.flush();
        } catch (IOException e) {
            log.error("fastDFS文件删除失败：" + str);
        }
    }
}
